<?php

/**
 * @file
 * Handle ranking votes, e.g. choice A is preferred over choice B, which in turn is preferred over choice C.
 */
function advpoll_info_ranking() {
    return array(
        'name' => 'ranking',
        'name_label' => t('Ranking'),
        'description' => t('Rank a number of choices.'),
    );
}

function advpoll_algorithms_ranking() {
    return array(
        'borda_count' => t('Borda count'),
        'instant_runoff' => t('Instant runoff'),
    );
}

function advpoll_voting_ranking_form(&$form_state, $node, $teaser, $page, $status) {
    static $ranking_form_count = 0;

    $form = array(
        '#id' => 'advpoll_voting_ranking_form-' . $ranking_form_count++,
        '#attributes' => array('class' => 'advpoll-vote'),
        '#node' => $node,
    );

    // Add write-in select box if write-ins are enabled and user has permission.
    $handle_writeins = $node->writeins && user_access('add write-ins');

    $form['ajax'] = array(
        '#type' => 'hidden',
        '#attributes' => array('class' => 'ajax'),
    );

    $form['#attributes']['class'] .= ' drag-and-drop';
    $form['js_order'] = array(
        '#type' => 'hidden',
        '#value' => '',
    );
    if ($node->max_choices) {
        $max_choices = $node->max_choices;
    } else {
        $max_choices = count($node->choice) - $node->writein_choices;
        /* if ($handle_writeins) {
          $max_choices++;
          }
         */
    }
    $form['max_choices'] = array(
        '#type' => 'hidden',
        '#value' => $max_choices,
    );

    // TODO: figure out why this is here. shouldn't every poll have choices?
    if (isset($node->choice)) {
        $list = array();
        $num_choices = count($node->choice);

        // Generate the list of possible rankings
        $choices[0] = '--';
        for ($i = 1; $i <= $num_choices; $i++) {
            if ($i == 1) {
                $val = t('1st');
            } elseif ($i == 2) {
                $val = t('2nd');
            } elseif ($i == 3) {
                $val = t('3rd');
            } else {
                $val = t($i . 'th');
            }
            $choices[$i] = $val;
        }
        // Fix to work around limitations in the current translation system. By 
        // listing the strings here they are made immediately available for 
        // translating. Listing up to 15 here, as it should be enough for most 
        // users. If more are needed they are made available for translating in 
        // Drupal when a poll with more than 15 choices has been created.
        // TODO: Find a better solution for this.
        array(t('4th'), t('5th'), t('6th'), t('7th'), t('8th'), t('9th'), t('10th'), t('11th'), t('12th'), t('13th'), t('14th'), t('15th'));

        // List of poll choices, to be populated.
        $form['choice'] = array('#tree' => TRUE);

        // If previewing check the format against the current users permissions.
        $check = $node->build_mode == NODE_BUILD_PREVIEW;
        foreach ($node->choice as $key => $choice) {
            // Don't show blank choices or write-in votes if the setting is disabled.
            if ($choice['label'] && ($node->show_writeins || !$choice['writein'])) {
                $form['choice'][$key] = array(
                    '#type' => 'select',
                    '#title' => _advpoll_choice_markup($choice['label'], $node->format, $check) . ($choice['writein'] ? ' ' . t('(write-in)') : ''),
                    '#options' => $choices,
                );
            }
        }

        if ($handle_writeins) {
            $form['choice'][$key + 1] = array(
                '#type' => 'select',
                '#title' => t('(write-in)'),
                '#options' => $choices,
                '#attributes' => array('class' => 'advpoll-writeins'),
            );
            // Key index of the write-in option.
            $form['writein_key'] = array(
                '#type' => 'value',
                '#value' => $key + 1,
            );
        }
    }

    // Add write-in text field.
    if ($handle_writeins) {
        $form['writein_choice'] = array(
            '#type' => 'textfield',
            '#title' => t('Write-in vote'),
            '#size' => 25,
        );
    }

    $form['nid'] = array(
        '#type' => 'hidden',
        '#value' => $node->nid,
        '#attributes' => array('class' => 'edit-nid'),
    );

    $advpollSettings = variable_get('advpoll_settings', array());
    $votingMode = (!empty($advpollSettings['voting_mode'])) ? $advpollSettings['voting_mode'] : '0';
    $showResults = (!empty($advpollSettings['show_results'])) ? $advpollSettings['show_results'] : 'aftervote';

    $displayButton = true;

    // no need to show vote button if user isn't actually able to vote
    if ($votingMode == 1 && isset($_COOKIE['advpollvote' . $node->nid])) {
        $displayButton = false;
        $duration = (!empty($advpollSettings['cookie_duration'])) ? $advpollSettings['cookie_duration'] : 60;
    }

    $voted = _advpoll_user_voted($node->nid);

    // Hide vote button if user can't vote and instead display appropriate message.
    if ($node->build_mode != NODE_BUILD_PREVIEW && advpoll_eligible($node) && $status == 'open' && $displayButton && !$voted[0]) {
        static $ranking_vote_count = 0;
        $form['vote'] = array(
            '#type' => 'submit',
            '#value' => t('Vote'),
            '#id' => 'edit-vote-rank-' . $ranking_vote_count++,
        );
    } elseif (!$displayButton) {
        $form['message']['#value'] = t('There is a delay of @time minutes between votes. Try again later.', array('@time' => $duration));
    } elseif ($status == 'pending') {
        $form['message']['#value'] = t('This poll opens @time.', array('@time' => format_date($node->start_date)));
    } elseif ($node->build_mode == NODE_BUILD_PREVIEW || $voted[0]) {
        if ($voted[0] && !user_access('cancel own vote')) {
            $form['message']['#value'] = (!empty($advpollSettings['vote_completed'])) ? $advpollSettings['vote_completed'] : t('Thank you for voting.');
        }
        // Display nothing.
    } else {
        global $user;
        $login_message = t('<a href="@login">Login</a> to vote in this poll.', array('@login' => url('user/login', array('query' => drupal_get_destination()))));
        $form['message']['#value'] = $user->uid ? t('You are not eligible to vote in this poll.') : $login_message;
    }

    $form['#action'] = url('node/' . $node->nid);
    // Set form caching because we could have multiple forms on the page.
    // (from poll.module).
    $form['#cache'] = TRUE;

    return $form;
}

/**
 * Process variables for advpoll-display-ranking-form.tpl.php.
 *
 * The variables array contains the following arguments:
 * - $form
 *
 * @see advpoll-display-ranking-form.tpl.php
 */
function advpoll_preprocess_advpoll_voting_ranking_form(&$variables) {
    $form = &$variables['form'];
    $variables['message'] = drupal_render($form['message']);

    // If write-ins are used on this form.
    if (isset($form['writein_choice'])) {
        $variables['writein_choice'] = drupal_render($form['writein_choice']);
    }
    $variables['footer_message'] = $form['#node']->footer_message;
    $variables['form_id'] = $form['#id'];

    // List of available choices in the poll.
    $variables['choice_list'] = drupal_render($form['choice']);
    // Take off the annoying colon & endlines that Drupal adds to each title.
    $variables['choice_list'] = preg_replace('/[\n\r]*: <\/label>[\n\r]*/i', '</label>', $variables['choice_list']);
    // All remaining form elements.
    $variables['form_submit'] = drupal_render($form);

    // Add tabledrag JavaScript.
    drupal_add_tabledrag($form['#id'] . '-table', 'order', 'self', 'advpoll-choice-order', NULL, NULL, FALSE);
}

function advpoll_view_results_ranking($node, $teaser, $page) {
    $results = votingapi_select_results(array('content_type' => 'advpoll', 'content_id' => $node->nid));
    $round_table = '';

    // If no one has voted, $results = array() and thus is empty.
    if (!empty($results)) {
        // Temporary list of choices indexes for the ranking.
        $ranking_list = array();
        // Result object
        $ranking = array();
        $choices = array();
        $poll = array();
        $rounds = array();
        foreach ($results as $result) {
            $tag = $result['tag'];
            if ($tag == '_advpoll') {
                // Poll-wide cached value.
                $poll[$result['function']] = $result['value'];
            } else if (strstr($tag, '_rounds_')) {
                // Re-construct round data and extract the round from the tag.
                $round = str_replace('_rounds_', '', $tag);
                if (!isset($rounds[$round])) {
                    $rounds[$round] = array();
                }
                // $result->function actually stores $choice.
                $rounds[$round][$result['function']] = $result['value'];
            } else if (isset($node->choice[$tag])) {
                // Note: choices that have been removed will not pass the previous
                // line's test even though their values are still in the vote table.
                // Choice-specific cached value.
                if ($result['function'] == 'ranking') {
                    $ranking_list[$result['value']][] = $tag;
                } else if (!isset($node->choice[$result['function']])) {
                    $choices[$tag][$result['function']] = $result['value'];
                }
            }
        }

        // Re-construct the rankings object.
        foreach ($ranking_list as $i => $choice_list) {
            $ranking[$i]->choices = array();
            foreach ($choice_list as $choice_i) {
                $ranking[$i]->choices[] = $choice_i;
                if (isset($choices[$choice_i]['view_score'])) {
                    $ranking[$i]->view_score = $choices[$choice_i]['view_score'];
                }
                if (isset($choices[$choice_i]['raw_score'])) {
                    $ranking[$i]->raw_score = $choices[$choice_i]['raw_score'];
                }
                if (isset($choices[$choice_i]['percentage'])) {
                    $ranking[$i]->percentage = $choices[$choice_i]['percentage'];
                }
            }
        }

        $output = '';

        if ($node->algorithm == 'borda_count') {
            for ($i = 0; $i < count($ranking); $i++) {
                $first_one = TRUE;
                $this_rank = '';

                // Loop through all choices with this ranking.
                foreach ($ranking[$i]->choices as $choice) {
                    $label = isset($node->choice[$choice]) ? _advpoll_choice_markup($node->choice[$choice]['label'], $node->format, FALSE) . ($node->choice[$choice]['writein'] ? ' ' . t('(write-in)') : '') : t('(deleted)');
                    $this_rank .= ($first_one ? '' : ', ') . $label;
                    $first_one = FALSE;
                }

                $percentage = round(100 * (isset($ranking[$i]->percentage) ? $ranking[$i]->percentage : 0), 0);
                $output .= theme('advpoll_bar', $this_rank, $percentage, $ranking[$i]->view_score);
            }
        } else {
            $output .= '<ol>';
            for ($i = 0; $i < count($ranking); $i++) {
                $output .= '<li> ';
                $first_one = TRUE;
                // If previewing check the format against the current users permissions.
                $check = $node->build_mode == NODE_BUILD_PREVIEW;

                // Loop through all choices with this ranking.
                foreach ($ranking[$i]->choices as $choice) {
                    $label = isset($node->choice[$choice]) ? _advpoll_choice_markup($node->choice[$choice]['label'], $node->format, FALSE) . ($node->choice[$choice]['writein'] ? ' ' . t('(write-in)') : '') : t('(deleted)');
                    $output .= ($first_one ? '' : ', ') . $label;
                    $first_one = FALSE;
                }

                // Show the ranking's score if it exists (depends on algorithm).
                if (isset($ranking[$i]->view_score)) {
                    $output .= ' (' . $ranking[$i]->view_score . '%)';
                }
                $output .= '</li>';
            }
        }
        $output .= '</ol>';

        if (user_access('inspect all votes') && isset($rounds)) {
            if (count($rounds) > 0) {
                $header[0] = t('Rounds');
                $total_rounds = count($rounds);
                for ($i = 0; $i < count($rounds); $i++) {
                    $choices = $rounds[$i];
                    if ($i + 1 == $total_rounds) {
                        // This is the last round.
                        $header[$i + 1] = t('Final');
                    } else {
                        $header[$i + 1] = $i + 1;
                    }
                    if ($i == 0) {
                        $rows = array();
                    }
                    foreach ($node->choice as $key => $data) {
                        $rows[$key][0] = $data['label'];
                        $rows[$key][$i + 1] = isset($choices[$key]) && $choices[$key] ? $choices[$key] : '';
                    }
                }
                $round_table = theme('table', $header, $rows, array(), t('Per-round breakdown of votes for each choice'));
            }
        }
    }
    $output .= $round_table;

    return array('results' => $output, 'votes' => $poll['total_votes']);
}

/**
 * Calculate the results for a ranking poll based on the algorithm.
 *
 * @param $node
 *  The node object for the current poll
 *
 * @return 
 *   Should return an object that include the following attributes
 *   -results : 2d array listing the aggregate preference, including ties
 *   -rounds : 2d array listing the per-choice vote count for each round and
 *              a status message indicating who was eliminated
 *   -totalVoters : the total number of voters who participated
 */
function advpoll_calculate_results_ranking(&$cache, $node) {
    if ($node->algorithm == 'borda_count') {
        $results = _advpoll_calculate_bordacount($node);
    } else {
        $results = _advpoll_calculate_instantrunoff($node);
    }

    // Cache rankings.
    // API: $cache[$tag][$type][$function] = $value (0 is the default $type)
    if (isset($results->ranking)) {
        for ($i = 0; $i < count($results->ranking); $i++) {
            foreach ($results->ranking[$i]['choices'] as $choice) {
                $cache[$choice][0]['ranking'] = $i;
                if (isset($results->ranking[$i]['raw_score'])) {
                    $cache[$choice][0]['raw_score'] = $results->ranking[$i]['raw_score'];
                }
                if (isset($results->ranking[$i]['view_score'])) {
                    $cache[$choice][0]['view_score'] = $results->ranking[$i]['view_score'];
                }
                if (isset($results->ranking[$i]['percentage'])) {
                    $cache[$choice][0]['percentage'] = $results->ranking[$i]['percentage'];
                }
            }
        }
    }

    // Cache round results.
    if (isset($results->matrix)) {
        foreach ($results->matrix as $i => $round) {
            $key = '_rounds_' . $i;
            $cache[$key] = array();
            foreach ($round as $choice => $votes) {
                $cache[$key][0][$choice] = count($votes);
            }
        }
    }

    // Cache total votes.
    $cache['_advpoll'][0]['total_votes'] = isset($results->total_votes) ? $results->total_votes : 0;

    // Cache total points (if it exists).
    if (isset($results->total_points)) {
        $cache['_advpoll'][0]['total_points'] = $results->total_points;
    }
}

/**
 * Calculate the results using borda count.
 * 
 * @param $node
 *  The node object for the current poll.
 *
 * @return 
 *   Should return an object that include the following attributes
 *   -results : 2d array listing the aggregate preference, including ties
 *   -rounds : 2d array listing the per-choice vote count for each round and
 *              a status message indicating who was eliminated
 *   -totalVoters : the total number of voters who participated
 */
function _advpoll_calculate_bordacount($node) {
    $votes = array();
    // ORDER BY value ASC lets us ensure no gaps.
    $result = db_query("SELECT * FROM {votingapi_vote} v WHERE content_type = '%s' AND content_id = %d ORDER BY value ASC", 'advpoll', $node->nid);
    while ($vobj = db_fetch_object($result)) {
        $votes[] = $vobj;
    }

    if (count($votes) == 0) {
        // No votes yet.
        return array();
    }

    // Aggregate votes by user (uid if logged in, IP if anonymous)
    // in ascending order of value.
    $user_votes = array();

    foreach ($votes as $vote) {
        if ($vote->uid == 0) {
            // Anonymous user.
            $key = $vote->vote_source;
        } else {
            // Logged-in user.
            $key = $vote->uid;
        }
        $user_votes[$key][$vote->value] = $vote->tag;
    }

    $choice_votes = array();
    $total_choices = count($node->choice);
    $total_points = 0;
    // Loop through each user's vote
    foreach ($user_votes as $uid => $user_vote) {
        foreach ($user_vote as $ranking => $choice) {
            // Negative values are possible if choices were removed after vote
            $vote_value = max($total_choices - $ranking, 0);
            isset($choice_votes[$choice]) ? $choice_votes[$choice] += $vote_value : $choice_votes[$choice] = $vote_value;
            $total_points += $vote_value;
        }
    }

    // Add any remaining choices that received no votes.
    foreach ($node->choice as $i => $choice) {
        if (!isset($choice_votes[$i])) {
            // Didn't receive any votes
            $choice_votes[$i] = 0;
        }
    }

    // Sort descending (although there may be ties).
    arsort($choice_votes);

    // Figure out the final ranking.
    $ranking = array();
    $previous_total = -1;
    $cur_result = -1;

    foreach ($choice_votes as $choice => $total) {
        if ($total != $previous_total) {
            // Didn't tie with the previous score.
            $cur_result++;
        }
        $ranking[$cur_result]['choices'][] = $choice;
        $ranking[$cur_result]['raw_score'] = $total;
        $ranking[$cur_result]['view_score'] = format_plural($total, '1 point', '@count points');
        $ranking[$cur_result]['percentage'] = $total_points ? $total / $total_points : 0;
        $previous_total = $total;
    }

    $total_votes = count($user_votes);

    $result_obj->ranking = $ranking;
    $result_obj->total_votes = $total_votes;
    $result_obj->total_points = $total_points;
    return $result_obj;
}

/**
 * Calculate the results using instant-runoff voting.
 * 
 * @param $node
 *  The node object for the current poll.
 *
 * @return 
 *   Should return an object that include the following attributes.
 *   -results : 2d array listing the aggregate preference, including ties
 *   -rounds : 2d array listing the per-choice vote count for each round and
 *              a status message indicating who was eliminated
 *   -totalVoters : the total number of voters who participated
 */
function _advpoll_calculate_instantrunoff($node) {
    $votes = array();
    // ORDER BY value ASC lets us ensure no gaps.
    $result = db_query("SELECT * FROM {votingapi_vote} v WHERE content_type = '%s' AND content_id = %d ORDER BY value ASC", 'advpoll', $node->nid);
    while ($vobj = db_fetch_object($result)) {
        $votes[] = $vobj;
    }

    if (count($votes) == 0) {
        // No votes yet.
        return array();
    }

    // Aggregate votes by user (uid if logged in, IP if anonymous)
    // in ascending order of value.
    $user_votes = array();

    foreach ($votes as $vote) {
        if ($vote->uid == 0) {
            // Anonymous user.
            $key = $vote->vote_source;
        } else {
            // Logged-in user.
            $key = $vote->uid;
        }

        // Note: relies on ORDER BY value ASC in vote-getting SQL query.
        // Otherwise a later vote might have a lower value.
        $user_votes[$key][] = $vote->tag;
    }

    $total_votes = count($user_votes);

    // Log of 1st-place votes per choice in each round.
    $round_log = array();

    // Gradually append candidates as they are eliminated; end with the winner.
    $reverse_ranking = array();

    // If we eliminate one choice per round and have n choices, we should
    // not be able to do more than n - 1 rounds.
    $max_rounds = count($node->choice);
    for ($round = 0; $round < $max_rounds; $round++) {

        // Initialize cur_round.
        $cur_round = array();
        $total_choices = count($node->choice);

        foreach ($node->choice as $key => $data) {
            $cur_round[$key] = array();
        }

        // Loop through each user.
        foreach ($user_votes as $key => $user_vote) {
            // $user_vote[0] contains the user's first remaining preference.
            $cur_round[$user_vote[0]][] = $key;
        }

        if ($round == 0) {
            // This is the first round.
            // Any choices with no first-place votes are considered eliminated.
            foreach ($cur_round as $key => $choice_votes) {
                if (count($choice_votes) == 0) {
                    unset($cur_round[$key]);
                    $reverse_ranking[0]['choices'][] = $key;
                }
            }
        }

        // Add the current round to the matrix.
        $round_log[] = $cur_round;

        // Calculate the min and max number of votes.
        $min_votes = -1;
        $max_votes = 0;

        // Number of choices that have already been discarded.
        $num_discarded = 0;

        // Examine the number of votes each choice received this round.
        foreach ($cur_round as $ch => $choice_votes) {
            $num_votes = count($choice_votes);

            if ($num_votes > $max_votes) {
                $max_votes = $num_votes;
                // Store current winner in case it has a majority.
                $cur_winner = $ch;
            }

            // This choice has already been eliminated (theoretically)
            // so don't count it as the minimum.
            if ($num_votes == 0) {
                $num_discarded++; // XXX: Probably don't need this variable any more
            } else if ($num_votes != 0 && ($num_votes < $min_votes || $min_votes == -1)) {
                $min_votes = $num_votes;
            }
        }

        // If one choice has a majority of remaining users it wins.
        // Note: we use count($user_votes) because some users may have incomplete
        // ballots and may have already had all of their choices eliminated.
        if ($max_votes > count($user_votes) / 2) {

            // Prune out the winning choice if it's still in there.
            if (isset($cur_round[$cur_winner])) {
                unset($cur_round[$cur_winner]);
            }

            // Keep computing until we figure out all final rankings.
            while (count($cur_round) > 0) {
                // Loop through non-winning choices.
                $current_place = array();
                $min = -1;
                foreach ($cur_round as $ch => $choice_votes) {
                    // Choice has already been eliminated, just unset it.
                    if (count($choice_votes) == 0) {
                        unset($cur_round[$ch]);
                    } else if ($min == -1
                            || count($choice_votes) < $min) {
                        // New minimum.
                        $current_place = array($ch);
                        $min = count($choice_votes);
                    } else if (count($choice_votes) == $min) {
                        // Tied for minimum.
                        $current_place[] = $ch;
                    }
                }

                // current_place will be empty the first iteration if some
                // choices had no first-place votes and were eliminated
                // at the beginning.
                if (count($current_place) > 0) {
                    $reverse_ranking[]['choices'] = $current_place;
                    // Remove all choices that had the minimum.
                    foreach ($current_place as $ch_key) {
                        unset($cur_round[$ch_key]);
                    }
                }
            }

            // Save a reversed version of the round log to help compute winnerPercent.
            $revmat = array_reverse($round_log);

            // The winner finally gets added
            $reverse_ranking[]['choices'] = array($cur_winner);
            $index = count($reverse_ranking) - 1;
            $reverse_ranking[$index]['raw_score'] = round(count($revmat[0][$cur_winner]) * 100 / count($user_votes), 1);
            $reverse_ranking[$index]['view_score'] = $reverse_ranking[$index]['raw_score'] . '%';

            $result_obj->matrix = $round_log;
            $result_obj->total_votes = $total_votes;
            $result_obj->ranking = array_reverse($reverse_ranking);
            return $result_obj;
        }

        // Since we're still here, no one has won, so eliminate one of the
        // choices with the lowest number of votes.
        // Find all choices with the minimum number of votes
        $min_choices = array();
        foreach ($cur_round as $ch => $choice_votes) {
            if (count($choice_votes) == $min_votes) {
                $min_choices[] = $ch;
            }
        }

        // Randomly select the choice to eliminate out of the available choices.
        // TODO: due to the randomness, this result must be cached after each vote.
        $round_loser = array_rand($min_choices);

        $reverse_ranking[]['choices'] = array($min_choices[$round_loser]);

        // Loop through the users who voted for the loser and redistribute.
        foreach ($cur_round[$min_choices[$round_loser]] as $user_key) {
            // Remove their current first preference.
            array_shift($user_votes[$user_key]);

            // Keep eliminating first preference until we run out or find an choice
            // that hasn't been eliminated.
            while ($cur_round[$user_votes[$user_key][0]] == array() && count($user_votes[$user_key]) > 0) {
                array_shift($user_votes[$user_key]);
            }

            // If they have no more preferences, remove from list for simplicity.
            if (count($user_votes[$user_key]) == 0) {
                unset($user_votes[$user_key]);
            }
        }
    }
    // Loop detected. Signal user and record.
    drupal_set_message(t('Could not find a solution within @rounds iterations.', array('@rounds' => $max_rounds)));
    $result_obj->matrix = $round_log;
    $result_obj->total_votes = $total_votes;
    return $result_obj;
}

/**
 * Implementation of the vote hook for the runoff module.
 *
 * This takes care of registering the vote in runoff nodes.
 */
function advpoll_voting_ranking_form_submit($form, &$form_state) {
    $votes = array();
    $node = $form['#node'];

    // Do submission specific to writeins.
    if (isset($form_state['values']['writein_key'])) {
        _advpoll_writeins_voting_form_submit($node, $form_state, $votes, $form_state['values']['choice'][$form_state['values']['writein_key']]);
    }

    if (!$votes) {
        foreach ($form_state['values']['choice'] as $choice => $rank) {
            $vote = array();
            // Ignore write-in choice that has already been taken care of.
            if (!$node->writeins || !isset($form_state['values']['writein_key']) || $choice != $form_state['values']['writein_key']) {
                $vote['value'] = $rank;
                // A zero value indicates they didn't rank that choice.
                if ($vote['value'] != 0) {
                    $vote['value_type'] = 'option';
                    $vote['tag'] = $choice;
                    $vote['content_type'] = 'advpoll';
                    $vote['content_id'] = $node->nid;
                    $vote = _advpoll_votesettings($vote, $node->nid);
                    $votes[] = $vote;
                }
            }
        }
    }
    votingapi_set_votes($votes, array());
    _advpoll_vote_response($node, $form_state);
}

/**
 * Implementation of the vote validation hook for the runoff module.
 *
 * This checks if the submitted values are within range, if they are
 * not empty, and if they are not repeated.
 *
 * @returns boolean false on invalid forms, true otherwise.
 */
function advpoll_voting_ranking_form_validate($form, &$form_state) {
    $node = $form['#node'];
    $ajax = $form_state['values']['ajax'];

    // Check if user is eligible to vote.
    if (!advpoll_eligible($node)) {
        _advpoll_form_set_error('choice[', t('You are not allowed to vote in this poll.'), $ajax);
    }

    // Check if poll is active.
    if (!_advpoll_is_active($node)) {
        _advpoll_form_set_error('choice[', t('This poll is closed.'), $ajax);
    }

    // Whether the write-in option is selected.
    $writein_option = FALSE;
    $writein_text = isset($form_state['values']['writein_key']) ? $form_state['values']['writein_choice'] : '';

    // Check if user has already voted.
    list($voted, $cancel_vote) = _advpoll_user_voted($node->nid);
    if ($voted) {
        _advpoll_form_set_error('choice[', t('You have already voted in this poll.'), $ajax);
        // Redirect to the current poll node to view the poll result instead of the voting form. This is only
        // initiated for non-Ajax voting.
        drupal_goto('node/' . $node->nid);
    }

    // Array used to check which values are set.
    $set_values = array();

    $num_choices = 0;

    // Write-ins are enabled, user has permission, and the write-in box is checked.
    if ($node->writeins && user_access('add write-ins') && $form_state['values']['choice'][$form_state['values']['writein_key']]) {
        $num_choices++;
        // Set a flag for additional checks.
        $writein_option = TRUE;
    }

    foreach ($node->choice as $key => $choice) {
        // Count the number of choices that are ranked.
        if ($form_state['values']['choice'][$key]) {
            $num_choices++;
        }
        $int_value = intval($form_state['values']['choice'][$key]);
        // Mark this value as seen
        isset($set_values[$int_value]) ? $set_values[$int_value]++ : $set_values[$int_value] = 1;
        // Check range
        if ($int_value > count($node->choice) || $int_value < 0) {
            // TODO: clean up this error message
            $message = "Illegal rank for choice $key: $int_value (min: 1, max: " . count($node->choice) . ')';
            _advpoll_form_set_error('choice][' . $key, $message, $ajax);
        }
    }

    // Write-ins are enabled, user has permission, and the write-in box is checked.
    if ($writein_option) {
        $int_value = intval($form_state['values']['choice'][$form_state['values']['writein_key']]);
        // Mark this value as seen
        $set_values[$int_value]++;
        // Check range
        if ($int_value > count($node->choice) || $int_value < 0) {
            // TODO: clean up this error message
            $message = "Illegal rank for the write-in choice: $int_value (min: 1, max: " . count($node->choice) . ')';
            _advpoll_form_set_error('choice][' . $form_state['values']['writein_key'], $message, $ajax);
        }
    }

    // Do validation specific to writeins.
    _advpoll_writeins_voting_form_validate($node, $writein_option, $writein_text, $ajax);

    // Too many choices ranked.
    if ($node->max_choices != 0 && $num_choices > $node->max_choices) {
        $message = t('%num choices were selected but only %max are allowed.', array('%num' => $num_choices, '%max' => $node->max_choices));
        _advpoll_form_set_error('choice', $message, $ajax);
    }

    // Not enough choices ranked.
    $min_choices = 1;
    if ($num_choices < $min_choices) {
        _advpoll_form_set_error('choice', t('At least one choice must be selected.'), $ajax);
    }

    // Check that multiple choices are not set to the same value.
    foreach ($set_values as $val => $count) {
        if ($val != 0 && $count > 1) {
            $message = t('Multiple choices given the rank of %value.', array('%value' => $val));
            _advpoll_form_set_error('choice', $message, $ajax);
        }
    }
}

/**
 * Hook to handle a cancelled vote for a ranking poll.
 */
function advpoll_cancel_ranking($node, $user_vote) {
    // Remove choice if this was the last vote for a write-in.
    if ($node->writeins) {
        $recalculate = FALSE;
        foreach ($user_vote as $vote) {
            if ($node->choice[$vote['tag']]['writein']) {
                // Check if there are any other votes for this write-in.
                $count = db_result(db_query('SELECT COUNT(1) FROM {votingapi_vote} WHERE content_id = %d AND tag = %d', $node->nid, $vote['tag']));
                if ($count == 0) {
                    // Delete the write-in because no one else voted for it.
                    db_query('DELETE FROM {advpoll_choices} WHERE cid = %d', $vote['tag']);
                    $recalculate = TRUE;
                    watchdog('content', 'Removed write-in choice %choice after the last vote was cancelled.', array('%choice' => $node->choice[$vote['tag']]['label']));
                }
            }
        }
        if ($recalculate) {
            votingapi_recalculate_results('advpoll', $node->nid);
        }
    }
}
